package com.yyoo.doc.mypdf.sax;

import com.lowagie.text.*;
import com.lowagie.text.pdf.*;
import com.yyoo.doc.html.sax.AbsSaxHandler;
import com.yyoo.doc.html.sax.SaxHtmlElement;
import com.yyoo.doc.html.tag.HtmlTag;
import com.yyoo.doc.mypdf.MyPdfException;
import com.yyoo.doc.mypdf.builder.PdfElementBuilder;
import org.xml.sax.Attributes;

import java.util.LinkedList;
import java.util.NoSuchElementException;

/**
 * @author zzy
 * html 转 pdf 的 SAX 解析器
 */
public class MyPdfSaxHandler extends AbsSaxHandler {

    /**
     * 当前 PdfWriter
     */
    private final PdfWriter pdfWriter;

    /**
     * 当前 pdf 文档对象
     */
    private final Document document;

    /**
     * pdf 文档对象是否已经调用 open 方法
     */
    private boolean docIsOpen = false;

    /**
     * 栈，用于存储当前正在执行的元素
     */
    private final LinkedList<Element> pdfElStack;


    /**
     * SAX 解析器构造器
     * @param pdfWriter PdfWriter 对象
     * @param document PDF Document 对象
     * @param baseUri 要解析的 html 的基本路径
     */
    public MyPdfSaxHandler(PdfWriter pdfWriter, Document document, String baseUri){
        super(baseUri);
        this.pdfElStack = new LinkedList<>();
        this.document = document;
        this.pdfWriter = pdfWriter;
    }


    @Override
    public void start() {
        PdfSaxHtmlElement currentHtml = toPdfBean(this.getCurrentElement());

        if(HtmlTag.PAGE_NEW.equals(currentHtml.getTagName())){
            document.newPage();
            this.getHtmlElStack().pollLast();
            return;
        }


        Element parent;
        try {
            parent = pdfElStack.getLast();
        }catch (NoSuchElementException ignored){
            parent = null;
        }
        PdfElementBuilder builder = PdfElementBuilder.newInstance(currentHtml,parent,getCssStyle(currentHtml));
        if (builder == null){
            this.getHtmlElStack().pollLast();
            return;
        }
        Element el = builder.build();
        pdfElStack.add(el);
        currentHtml.setBuilder(builder);

    }

    @Override
    public void end(SaxHtmlElement curEl){
        PdfSaxHtmlElement currentHtml = toPdfBean(curEl);
        Element curPdfEl = pdfElStack.pollLast();
        if(curPdfEl == null || currentHtml.isIgnore()){
            return;
        }
        addText(curPdfEl,currentHtml);

        try {
            Element parent = pdfElStack.getLast();
            PdfSaxHtmlElement parentHtmlEl = toPdfBean(getHtmlElStack().getLast());

            addText(parent,parentHtmlEl);

            // 是否是嵌套表格
            boolean isNestedTable = isNestedTable(parent,curPdfEl);
            // 如果当前标签是嵌套表格
            if(isNestedTable){
                // 跳过其父标签(防止表格多添加空行)
                parentHtmlEl.setIgnore(true);
                parent = pdfElStack.get(pdfElStack.size() - 2);
            }

            addPdfElement(parent,parentHtmlEl,curPdfEl,currentHtml,isNestedTable);

        }catch (NoSuchElementException e){
            // getLast 方法会抛出 NoSuchElementException
            addDocument(curPdfEl,currentHtml);
        }


        /*
         * 可以设置书签的 html 标签：p, div, h1, h2, h3, h4, h5, h6
         * bookmarks属性规则：签规则：如 1-1-1 表示第一个一级标题的第一个子标题的第一个子标题，数字代表对应级别的标题顺序，顺序从1开始
         * bookmarks属性规则：数字代表第一个标题，中间的 - 表示标题的级别，- 前面的表示父级别，后面的表示子级别；每个书签必须包含其从最高级别到最低级别的所有数字，级别之间用 - 隔开
         * title属性：标题的标题，用于显示在书签上（如果不设置，则默认使用标签的文本内容为书签标题）
         */
        String bookmarks = currentHtml.getAttributeValue("bookmarks");
        if(bookmarks != null && !bookmarks.isEmpty()){
            String title = currentHtml.getAttributeValue("title");
            title = title == null ? currentHtml.getOwnTextString() : title;

            PdfOutline rootOutline = this.pdfWriter.getDirectContent().getRootOutline();
            new PdfOutline(getParentOutline(rootOutline,bookmarks), PdfAction.gotoLocalPage(bookmarks, false), title);
        }
    }

    /**
     * 重写父类方法，将当前 html 对象转换为 PdfSaxHtmlElement 对象
     * @param tagName    标签名
     * @param attributes 标签属性
     * @param ownText 标签文本
     * @param baseUri    当前文档的根路径
     * @return
     */
    @Override
    public SaxHtmlElement createHtmlElement(String tagName, Attributes attributes, StringBuilder ownText, String baseUri) {
        return new PdfSaxHtmlElement(tagName, attributes, ownText, baseUri);
    }

    /**
     * 将父类 SaxHtmlElement 对象转换为子类 PdfSaxHtmlElement 对象
     * @param saxHtmlElement 父类 SaxHtmlElement 对象
     * @return 子类 PdfSaxHtmlElement 对象
     */
    private PdfSaxHtmlElement toPdfBean(SaxHtmlElement saxHtmlElement){
        return (PdfSaxHtmlElement) saxHtmlElement;
    }

    /**
     * 判断当前元素是否是嵌套表格
     * @param parent 父元素
     * @param curPdfEl 当前 pdf 元素
     */
    private boolean isNestedTable(Element parent,Element curPdfEl){
        return parent instanceof Phrase &&  !(curPdfEl instanceof Phrase);
    }

    /**
     * 为文本类标签添加文本
     * @param el 当前 pdf 元素
     * @param htmlEl 当前 html 元素
     */
    private void addText(Element el, SaxHtmlElement htmlEl){
        // 先处理当前pdf元素的文本内容
        if(el instanceof Phrase && ((Phrase) el).isEmpty()) {
            String bookmarks = htmlEl.getAttributeValue("bookmarks");
            if(bookmarks != null && !bookmarks.isEmpty()){
                Chunk chunk = new Chunk(htmlEl.getOwnTextString(), ((Phrase) el).getFont());
                chunk.setLocalDestination(bookmarks);
                ((Phrase) el).add(chunk);
            }else {
                ((Phrase) el).add(htmlEl.getOwnTextString());
            }
        }
    }

    /**
     * 添加 pdf 元素到父元素
     * @param parentPdfEl 父元素
     * @param parentHtmlEl 父 html 元素
     * @param childEl PDF 元素
     * @param currentHtml 当前 html 元素
     * @param isNestedTable 是否是嵌套表格
     */
    private void addPdfElement(Element parentPdfEl, PdfSaxHtmlElement parentHtmlEl, Element childEl,PdfSaxHtmlElement currentHtml, boolean isNestedTable){
        // 添加文本段落到父元素
        if(parentPdfEl instanceof Phrase){
            ((Phrase) parentPdfEl).add(childEl);
        }else if(parentPdfEl instanceof HeaderFooter){
            // 如果父标签是页眉或页脚，则通过特殊方式添加子元素
            ((HeaderFooter) parentPdfEl).addSpecialContent(childEl);
        }else if(parentPdfEl instanceof List){
            // 如果父标签是列表，则添加子元素
            if(childEl instanceof List) {
                ((List) parentPdfEl).add((List)childEl);
            }else {
                ((List) parentPdfEl).add(childEl);
            }
        }else if(parentPdfEl instanceof PdfPTable){
            if(childEl instanceof Phrase) {
                // 如果是文本段落，则添加单元格
                ((PdfPTable) parentPdfEl).addCell((Phrase)childEl);
            }else if(childEl instanceof PdfPCell){
                // 如果是单元格对象
                ((PdfPTable) parentPdfEl).addCell((PdfPCell)childEl);
            }else if(childEl instanceof PdfPTable){
                // 如果是嵌套表格对象
                ((PdfPTable) parentPdfEl).addCell((PdfPTable)childEl);
            }else if(childEl instanceof Image){
                // 如果是图片对象
                ((PdfPTable) parentPdfEl).addCell((Image)childEl);
            }
        }
        // 执行构建器中的 afterBuild 方法
        currentHtml.getBuilder().afterBuild(childEl);

        // 注：此操作只支持 body 标签下的表格，不支持嵌套表格（意味着嵌套表格不宜过大）
        // 因为嵌套的表格需要先添加到父级表格中，要等到父级表格 document add 之后才会刷新内存（所以嵌套表格无法支持过大刷新内存）
        // 如果父标签是表格（当前标签是单元格，在添加到表格后，如果表格行数超过 1000 则先将内存中的 table 内容 add 到 document，然后清空，避免高内存占用）
        // 这里的 1000 为试验值，提高该值并不能明显提升性能
        if(!isNestedTable && parentPdfEl instanceof PdfPTable && ((PdfPTable)parentPdfEl).size() > 1000){
            parentHtmlEl.getBuilder().afterBuild(parentPdfEl);
            openDocument();
            document.add(parentPdfEl);
            // 将 table 内容 add 到 document 后，执行 completeRow，再执行 flushContent
            // 将已添加内容清空，然后继续添加 cell（避免超大量的 cell 添加时一直存在于内存，导致内存溢出）
            ((PdfPTable)parentPdfEl).completeRow();
            ((PdfPTable)parentPdfEl).flushContent();
        }

    }

    /**
     * 添加根元素（body）下的 pdf 元素到 document
     * @param curPdfEl 当前 pdf 元素
     * @param curEl 当前 html 元素
     */
    private void addDocument(Element curPdfEl, PdfSaxHtmlElement curEl) {
        curEl.getBuilder().afterBuild(curPdfEl);
        if(curPdfEl instanceof HeaderFooter && HtmlTag.PAGE_HEADER.equals(curEl.getTagName())) {
            document.setHeader((HeaderFooter)curPdfEl);
            openDocument();
        }else if (curPdfEl instanceof HeaderFooter && HtmlTag.PAGE_FOOTER.equals(curEl.getTagName())) {
            document.setFooter((HeaderFooter)curPdfEl);
            openDocument();
        } else {
            openDocument();
            document.add(curPdfEl);
        }
    }

    /**
     * 打开 document
     * PDF 文档正文内容，需要先 open 之后才能添加
     * 但如果在打开之后才设置修改 header 和 footer 则第二页才会生效
     */
    private void openDocument() {
        if(!docIsOpen){
            document.open();
            docIsOpen = true;
        }
    }

    /**
     * 获取父级 outline
     * @param rootOutline 根级 outline
     * @param localDestination 层级 bookmarks 属性
     * <blockquote><pre>
     * 书签规则：如 1-1-1 表示第一个一级标题的第一个子标题的第一个子标题
     * 数字代表第一个标题，中间的 - 表示标题的级别
     * </pre></blockquote>
     */
    private PdfOutline getParentOutline(PdfOutline rootOutline,String localDestination){
        String[] arr = localDestination.split("-");

        if(arr.length == 0){
            throw new MyPdfException("请按规则添加 bookmarks 属性");
        }

        PdfOutline rs = rootOutline;
        for(int i = 0; i < arr.length - 1; i++){
            rs = rs.getKids().get(Integer.parseInt(arr[i]) - 1);
        }

        return rs;
    }

}
